LBB

使用 Svelte 开发 WebComponents

# 使用 Svelte 开发 WebComponents ## Base - **S****velte3** **https://svelte.dev** ### 完整 Demo https://github.com/lbb00/svelte-web-compnents-demo ## 为什么使用 Svelte - 书写更少的代码 - 没有虚拟 DOM,没有运行时,生成的代码体积非常小 其实,使用 Vue3 开发 WebComponents 也可以,可以参考👇🏻的分析: https://github.com/yyx990803/vue-svelte-size-analysis ## 开发 ### 创建项目 #### 创建 Svelte 项目 ```shell npm create vite@latest myapp -- --template svelte ``` #### 在 src/lib 下添加 Button.svelte 和 Loading.svelte 两个组件 ```html <!-- Button.svelte --> <script> import { createEventDispatcher } from 'svelte' import Loading from './Loading.svelte' const dispatch = createEventDispatcher() export let text = '' export let loading = false function onClick(e) { dispatch('tap', e.detail) } </script> <button class="custom-button" on:click={onClick}> {#if loading} <Loading /> {/if} <slot>{text}</slot> </button> <style> .custom-button { font-size: 2rem; background-color: #256eff; box-shadow: 0px 15px 27px 2px rgba(37, 110, 255, 0.28); border: none; outline: none; display: flex; align-items: center; justify-content: center; padding: 1rem 3rem; border-radius: 1rem; color: #fff; cursor: pointer; } </style <!-- Loading.svelte --> <span class="loading" /> <style> .loading { display: block; width: 25px; height: 25px; margin-right: 10px; border-radius: 50%; border: 3px solid #fff; border-color: #fff transparent #fff transparent; animation: ring 1.2s linear infinite; } @keyframes ring { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> ``` #### 修改 app.svelte ```html <script> import CustomButton from './lib/Button.svelte' let loading = false function onTap(e) { loading = true setTimeout(() => { loading = false }, 2000) } </script> <main> <CustomButton bind:loading on:tap={onTap}>Start</CustomButton> </main> ``` #### 运行起来 执行 `npm run dev` 后打开网页会展示下图的按钮,demo 项目就准备好了。 点击 Start ,就会出现 loading 动画。 ![img](https://uoyguvbzfk.feishu.cn/space/api/box/stream/download/asynccode/?code=MTk1MWQ3MWVhOGVmNzJiMjJjMWE2ZmU0YmE0ZjY4ZWFfOGFnOE5oSEJqaEozdngzN1hMVEFMT0Vocm04UlVTUnJfVG9rZW46Ym94Y25PMDFZQ3NjMUF0Nk9IaVYwRmVlaTVmXzE3MjM2MTg1ODY6MTcyMzYyMjE4Nl9WNA) ### 构建为 WebComponents #### 修改 vite.config.js 和 package.json 需要注意的是,如果组件中一部分作为 WebCopmonents,一部分内部组件不作为 WebCopmonents,将不作为 WebComponents 的组件设置 `<svelte:options tag={null} />` 构建后,使用时会遇到 `Uncaught TypeError: Illegal constructor` 的错误。 参考 https://github.com/sveltejs/svelte/issues/3594 解决该问题。 ```javascript // vite.config.js import { resolve } from 'node:path' import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' const isLib = process.env.MODE === 'lib' const dev = defineConfig({ plugins: [svelte()], }) const lib = defineConfig({ build: { lib: { entry: resolve(__dirname, 'src/lib/Button.svelte'), name: 'CustomButton', fileName: 'button', }, }, plugins: [ svelte({ compilerOptions: { customElement: true, }, include: 'src/lib/Button.svelte', }), svelte({ compilerOptions: { customElement: false, }, // 避免产生独立的 css 文件 emitCss: false, exclude: 'src/lib/Button.svelte', }), ], }) export default isLib ? lib : dev { // ... "scripts": { // build 时新增 MODE=lib 的 env "build": "MODE=lib vite build", } //... } ``` #### 为自定义组件添加 <svelte:options tag="..." /> ```html <!-- Button.svelte --> <svelte:options tag="custom-button" /> ``` #### 解决 dispatch 事件不生效 https://github.com/sveltejs/svelte/issues/3119#issuecomment-588566575 把作为 WebComponents 组件中的 dispatch 替换为如下代码 ```javascript import { createEventDispatcher } from 'svelte' import { get_current_component } from 'svelte/internal' const component = get_current_component() const svelteDispatch = createEventDispatcher() const dispatch = (name, detail) => { component.dispatchEvent && component.dispatchEvent(new CustomEvent(name, { detail })) return svelteDispatch(name, detail) } ``` #### Props Svelte 构建的 WebComponents 组件无法将属性类似 'foo-bar' 转化为 'fooBar' 的 props, 因此需要时用 'foobar' 或 'foo_bar' 作为 props。 #### 构建 执行 `npm run build` ### 在其他框架中使用 WebCompnents 组件 #### Vue 可以查看 demo 项目中的 demo.vue.html ```html <!DOCTYPE html> <html> <body> <div id="app"> <custom-button :loading="loading" @tap="onTap">Start</custom-button> </div> </body> <script src="./dist/button.umd.cjs"></script> <script type="module"> import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js' createApp({ setup() { const loading = ref(false) function onTap(e) { loading.value = true setTimeout(() => { loading.value = false }, 2000) } return { loading, onTap, } }, }).mount('#app') </script> </html> ``` #### React 可以查看 demo 项目中的 demo.react.html ```html <!DOCTYPE html> <html> <div id="app"></div> <body> <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script src="./dist/button.umd.cjs"></script> <script type="text/babel"> const Demo = () => { // loading 不能设置为 false,因为会作为字符串’false'传递给组件 const [loading, setLoading] = React.useState('') const ref = React.useRef() React.useLayoutEffect(() => { const onTap = (e) => { setLoading(true) setTimeout(() => { setLoading('') }, 2000) } const { current } = ref current.addEventListener('tap', onTap) return () => { current.removeEventListener('tap', onTap) } }, [ref]) return ( <custom-button loading={loading} ref={ref}> Start </custom-button> ) } const container = document.getElementById('app') const root = ReactDOM.createRoot(container) root.render(<Demo />) </script> </body> </html> ```
使用 Svelte 开发 WebComponents | LBB